package com.tools.web.interceptor.auth;

import com.fasterxml.jackson.core.type.TypeReference;
import com.tools.common.object.json.JsonKit;
import com.tools.common.object.Note;
import com.tools.common.object.string.VerifyKit;
import com.tools.common.thread.Timeunit;
import com.tools.web.interceptor.access.AccessManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * 使用 RedisTemplate 操作 Redis 客户端
 * 完成一个用户多个终端在线的用户认证管理器
 * */
@Note("使用 RedisTemplate 操作 Redis 客户端" +
        "完成一个用户多个终端在线的用户认证管理器")
public final class MultipleRedisTemplateAuthenticationManager extends GenericAuthenticationManager {

    @Note("使用 RedisTemplate 操作 Redis 客户端完成用户认证等操作")
    private final RedisTemplate<String, String> redisTemplate;

    @Note("设备类型 ID 与这种设备能同时在线的最大数量")
    private final Map<String, Integer> deviceTypeIdAndMaxCount;

    @Note("日志对象")
    private static final Logger LOGGER = LoggerFactory.getLogger(MultipleJedisClusterPlusAuthenticationManager.class);

    /* **************************************************************************************
     *
     *          构造方法
     *
     * **************************************************************************************
     * */

    public MultipleRedisTemplateAuthenticationManager(RedisTemplate<String, String> redisTemplate, AuthSource source,
                                                      AuthUserService authUserService) {
        this(redisTemplate, source, authUserService, null);
    }

    public MultipleRedisTemplateAuthenticationManager(RedisTemplate<String, String> redisTemplate, AuthSource source,
                                                      AuthUserService authUserService, AccessManager accessManager) {
        super(source, authUserService, accessManager);
        this.redisTemplate = Objects.requireNonNull(redisTemplate);
        String json = source.getDeviceTypeIdAndMaxTotalJsonString();
        if(VerifyKit.isJsonObject(json)) {
            this.deviceTypeIdAndMaxCount = JsonKit.toEntity(json, new TypeReference<HashMap<String, Integer>>() {});
            this.protected_checkDeviceTypeIdAndMaxCountMap(this.deviceTypeIdAndMaxCount);
            return;
        }
        throw new IllegalArgumentException(json + " 不是 JSON 对象类型的字符串");
    }

    /* **************************************************************************************
     *
     *          功能方法
     *
     * **************************************************************************************
     * */

    @Override
    protected Logger protected_getLogger() {
        return LOGGER;
    }

    @Override
    protected Timeunit protected_getTimeunitToBeUsed() {
        return Timeunit.SECOND;
    }

    @Override
    protected void protected_loginOut(String key, String deviceTypeId, String uuid,
                                      AuthUser authUser, String separator) {
        String script = "local prefix = ARGV[2];" +
                        "local keys = redis.call('keys', prefix .. '*');" +
                        "redis.call('del', ARGV[1]);" +
                        "if #keys == 0 then" +
                        "    redis.call('del', ARGV[3]);" +
                        "end";
        String tokenKey = key + separator + deviceTypeId + separator + uuid;
        this.redisTemplate.execute((RedisConnection connection) -> connection.eval(
                script.getBytes(),
                ReturnType.VALUE,
                0,
                tokenKey.getBytes(),
                key.getBytes(),
                (key + separator + USER_DATA_NAME).getBytes()
        ));
    }

    @Override
    protected Res getAuthUser(String key, AuthToken token) {
        String script =
                "local key1 = KEYS[1];" +
                "local key2 = KEYS[2];" +
                "if redis.call('exists', key1) == 1 then" +
                "    return redis.call('get', key2);" +
                "else" +
                "    return nil;" +
                "end";
        String uuid = token.getUuid();
        String deviceTypeId = token.getDeviceTypeId();
        String separator = token.getSeparator();
        String tokenKey = key + separator + deviceTypeId + separator + uuid;
        String authUserKey = key + separator + USER_DATA_NAME;
        Object result = this.redisTemplate.execute((RedisConnection connection) -> connection.eval(
                script.getBytes(),
                ReturnType.VALUE,
                2,
                tokenKey.getBytes(),
                authUserKey.getBytes()
        ));
        if(result == null) {
            return super.protected_getResIfExpired(token);
        }
        String json = result.toString();
        MultiAuthUser authUser = JsonKit.toEntity(json, MultiAuthUser.class);
        authUser.setUuid(uuid);
        authUser.setDeviceTypeId(deviceTypeId);
        authUser.setUserType(token.getUserType());
        return new Res(true, authUser);
    }

    @Override
    protected void setAuthUser(String key, AuthUser srcAuthUser, AuthToken token) {
        String script = "local keys = redis.call('KEYS', ARGV[1] .. '*');" +
                        "local expiration = tonumber(ARGV[5]);" +
                        "if #keys < tonumber(ARGV[2]) then" +
                        "    redis.call('SET', ARGV[3], ARGV[4]);" +
                        "    redis.call('EXPIRE', ARGV[3], expiration);" +
                        "else" +
                        "    local min_timestamp = math.huge;" +
                        "    local min_key = nil;" +
                        "    for _, key in ipairs(keys) do" +
                        "        local timestamp = tonumber(redis.call('GET', key));" +
                        "        if timestamp and timestamp < min_timestamp then" +
                        "            min_timestamp = timestamp;" +
                        "            min_key = key;" +
                        "        end" +
                        "    end" +
                        "    if min_key then" +
                        "        redis.call('DEL', min_key);" +
                        "    end" +
                        "    redis.call('SET', ARGV[3], ARGV[4]);" +
                        "    redis.call('EXPIRE', ARGV[3], expiration);" +
                        "end" +
                        "local ttl = redis.call('TTL', ARGV[6]);" +
                        "if ttl <= 0 then" +
                        "    redis.call('SET', ARGV[6], ARGV[7]);" +
                        "    redis.call('EXPIRE', ARGV[6], expiration);" +
                        "else" +
                        "    redis.call('SET', ARGV[6], ARGV[7]);" +
                        "    redis.call('EXPIRE', ARGV[6], ttl + expiration);" +
                        "end";
        String separator = token.getSeparator();
        String authUserKey = key + separator + USER_DATA_NAME;
        String deviceTypeId = token.getDeviceTypeId();
        String deviceKeyPrefix = key + separator + deviceTypeId;
        String tokenKey = deviceKeyPrefix + separator + token.getUuid();
        int maxSize = this.deviceTypeIdAndMaxCount.get(deviceTypeId);
        long loginTime = System.currentTimeMillis();
        String authUserValue = JsonKit.toJSON(srcAuthUser);
        this.redisTemplate.execute((RedisConnection connection) -> connection.eval(
                script.getBytes(),
                ReturnType.VALUE,
                0,
                deviceKeyPrefix.getBytes(),
                String.valueOf(maxSize).getBytes(),
                tokenKey.getBytes(),
                String.valueOf(loginTime).getBytes(),
                String.valueOf(token.getExpiration()).getBytes(),
                authUserKey.getBytes(),
                authUserValue.getBytes()
        ));
    }

    @Override
    protected void removeAuthUser(String key) {
        String script = "local prefix = ARGV[1];" +
                        "local keys = redis.call('keys', prefix .. '*');" +
                        "local count = 0;" +
                        "for i, key in ipairs(keys) do" +
                        "    redis.call('del', key);" +
                        "    count = count + 1;" +
                        "end" +
                        "return count;";
        this.redisTemplate.execute((RedisConnection connection) -> connection.eval(
                        script.getBytes(),
                        ReturnType.INTEGER,
                        0,
                        key.getBytes()
                ));
    }

    @Override
    protected void updateAuthUser(String key, AuthUser authUser, String separator) {
        String script =
                "local key = ARGV[1];" +
                "local newValue = ARGV[2];" +
                "if redis.call('exists', key) == 1 then" +
                "    local ttl = redis.call('ttl', key);" +
                "    redis.call('set', key, newValue);" +
                "    if ttl > 0 then" +
                "        redis.call('expire', key, ttl);" +
                "    end" +
                "    return 1;" +
                "else" +
                "    return 0;" +
                "end";
        String authUserKey = key + separator + USER_DATA_NAME;
        String authUserValue = JsonKit.toJSON(authUser);
        this.redisTemplate.execute((RedisConnection connection) -> connection.eval(
                        script.getBytes(),
                        ReturnType.INTEGER,
                        0,
                        authUserKey.getBytes(),
                        authUserValue.getBytes()
        ));
    }
}
